﻿Imports System
Imports System.Net
Imports System.Net.sockets
Imports System.Threading

Namespace TCP

    Public Class ClientSocket

        Protected _socket As Socket
        Protected _buffer() As Byte
        Protected _thread As Thread
        Protected _isReceiving As Boolean
        Protected _isDisconnected As Boolean = True
        Protected _isFreeTime As Boolean = False
        Protected _lastTicks As Long
        Protected _freeTicks As Long
        Protected _heartData() As Byte
        Protected _heartIdleTime As Integer               '心跳包接受发送时间
        Protected _heartWaitTime As Integer               '心跳包接受等待服务器响应间隔
        Protected _heartRep As Integer                    '等待服务器次数
        Protected _heartDataClient() As Byte = Text.Encoding.ASCII.GetBytes("Hearting") '客户端心跳包数据
        Protected _heartDataServer() As Byte = _heartDataClient '服务端心跳包数据
        Protected _receiveBufferSize As Integer = 512
        Protected _sendBufferSize As Integer = 512
        Protected _receiveTimeout As Integer = 0
        Protected _sendTimeout As Integer = 0
        Protected _encoding As Text.Encoding = Text.Encoding.ASCII
        'Protected _receiveLockObject As New Object()
        Protected _isBusy As Boolean = False


        Public Event OnConnect As EventHandler(Of ConnectEventArgs)
        Public Event OnReceive As EventHandler(Of ReceiveEventArgs)
        Public Event OnDisconnect As EventHandler(Of DisconnectArgs)
        Public Event OnFreeTime As EventHandler
        Public Event OnHeartWait As EventHandler(Of HeartWaitArgs)
        Public Event OnHeartTimeout As EventHandler


#Region "Properties"

        ''' <summary>
        ''' 客户端Socket对象
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property Socket() As Socket
            Get
                Return _socket
            End Get
        End Property

        ''' <summary>
        ''' 数据接收缓冲区大小(字节)。默认512字节。
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property ReceiveBufferSize() As Integer
            Get
                Return _receiveBufferSize
            End Get
            Set(ByVal value As Integer)
                _receiveBufferSize = value
            End Set
        End Property

        ''' <summary>
        ''' 数据发送缓冲区大小(字节)。默认512字节。
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property SendBufferSize() As Integer
            Get
                Return _sendBufferSize
            End Get
            Set(ByVal value As Integer)
                _sendBufferSize = value
            End Set
        End Property

        ''' <summary>
        ''' 数据接收超时值(毫秒)。默认值为 0，指示超时期限无限大。
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property ReceiveTimeout() As Integer
            Get
                Return _receiveTimeout
            End Get
            Set(ByVal value As Integer)
                _receiveTimeout = value
            End Set
        End Property

        ''' <summary>
        ''' 数据发送超时值(毫秒)。默认值为 0，指示超时期限无限大。
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property SendTimeout() As Integer
            Get
                Return _sendTimeout
            End Get
            Set(ByVal value As Integer)
                _sendTimeout = value
            End Set
        End Property

        ''' <summary>
        ''' 心跳包开始发送时间(毫秒)
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property HeartIdleTime() As Long
            Get
                Return _heartIdleTime / 10000
            End Get
            Set(ByVal value As Long)
                _heartIdleTime = value * 10000
            End Set
        End Property

        ''' <summary>
        ''' 心跳包发送等待服务器响应间隔(毫秒)
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property HeartWaitTime() As Long
            Get
                Return _heartWaitTime
            End Get
            Set(ByVal value As Long)
                _heartWaitTime = value
            End Set
        End Property

        ''' <summary>
        ''' 等待服务器响应次数
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property HeartRep() As Integer
            Get
                Return _heartRep
            End Get
            Set(ByVal value As Integer)
                _heartRep = value
            End Set
        End Property

        ''' <summary>
        ''' 服务器端心跳包数据
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property HeartDataServer() As Byte()
            Get
                Return _heartDataServer
            End Get
            Set(ByVal value As Byte())
                _heartDataServer = value
            End Set
        End Property

        ''' <summary>
        ''' 客户端心跳包数据
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property HeartDataClient() As Byte()
            Get
                Return _heartDataClient
            End Get
            Set(ByVal value As Byte())
                _heartDataClient = value
            End Set
        End Property

        ''' <summary>
        ''' 客户端是否断开
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property IsDisconnected() As Boolean
            Get
                Return _isDisconnected
            End Get
        End Property

        ''' <summary>
        ''' 当前缓冲区中可用数据长度
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property Available() As Integer
            Get
                Return _socket.Available
            End Get
        End Property

        ''' <summary>
        ''' 字符编码方式(默认ASCII)
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property Encoding() As Text.Encoding
            Get
                Return _encoding
            End Get
            Set(ByVal value As Text.Encoding)
                _encoding = value
            End Set
        End Property

#End Region


        Public Sub New()
        End Sub

        Private Sub InitSocket()
            _socket = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
            _socket.ReceiveBufferSize = _receiveBufferSize
            _socket.SendBufferSize = _sendBufferSize
            _socket.ReceiveTimeout = _receiveTimeout
            _socket.SendTimeout = _sendTimeout
        End Sub

        '连接服务器
        Public Function ConnectToServer(ByVal ipAddress As String, ByVal port As Integer) As Boolean
            Try
                Dim ipep As New IPEndPoint(Net.IPAddress.Parse(ipAddress), port)
                InitSocket()

                Debug.Write("ClientSocket.ConnectToServer() Connect " & ipAddress & ":" & port & vbCrLf)
                _socket.Connect(ipep)
                Debug.Write("ClientSocket.ConnectToServer() Connected OK" & vbCrLf)

                If _thread Is Nothing Then
                    _thread = New Thread(AddressOf WorkerThread)
                    _thread.IsBackground = True
                    _isReceiving = True
                    _thread.Start()
                End If

                _isDisconnected = False
                _isFreeTime = False
                RaiseEvent OnConnect(Me, New ConnectEventArgs(_socket.RemoteEndPoint))
            Catch ex As SocketException
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.ConnectToServer() Connect Failed" & vbCrLf)
                Return False
            Catch ex As Exception
                Debug.Write("ClientSocket.ConnectToServer() Connect Failed" & vbCrLf)
                Return False
            End Try

            Return True
        End Function

        '数据接收工作线程
        Private Sub WorkerThread()
            Dim bufLen As Integer = 0
            Dim nowTicks As Long = 0
            Dim times As Integer = 0
            _lastTicks = DateTime.Now.Ticks

            Do While _isReceiving
                nowTicks = DateTime.Now.Ticks


                If _isFreeTime Then
                    Do While times < _heartRep
                        If _isFreeTime = False OrElse _isBusy = True Then Exit Do
                        times += 1

                        If _isDisconnected = False Then RaiseEvent OnHeartWait(Me, New HeartWaitArgs(times))
                        Debug.Write("ClientSocket.WorkerThread() OnHeartWait " & times & vbCrLf)
                        BeginHeart()
                        If _isDisconnected Then Exit Do

                        'Thread.Sleep(_heartWaitTime)
                    Loop

                    If _isFreeTime Then
                        times = 0
                        If _isDisconnected = False Then RaiseEvent OnHeartTimeout(Me, EventArgs.Empty)
                        If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.HeartTimeout))
                        Debug.Write("ClientSocket.WorkerThread() OnHeartTimeout" & vbCrLf)
                        Exit Do
                    End If
                Else
                    '检测是否有数据
                    Try
                        If _socket.Poll(0, SelectMode.SelectRead) Then
                            bufLen = _socket.Available
                            If bufLen = 0 Then
                                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                                Debug.Write("ClientSocket.WorkerThread() Receive OnDisconnect" & vbCrLf)
                                Exit Do
                            Else
                                _isFreeTime = False
                                _lastTicks = DateTime.Now.Ticks
                                If _isDisconnected = False Then RaiseEvent OnReceive(Me, New ReceiveEventArgs(bufLen))
                                'Debug.Write("ClientSocket.WorkerThread() Receive OK" & vbCrLf)
                            End If
                        Else
                            '检测客户端空闲状态
                            Try
                                If (nowTicks - _lastTicks) > _heartIdleTime Then
                                    If _isFreeTime = False Then
                                        _freeTicks = DateTime.Now.Ticks
                                        _isFreeTime = True
                                        times = 0
                                        'BeginHeart()
                                        If _isDisconnected = False Then RaiseEvent OnFreeTime(Me, EventArgs.Empty)
                                        Debug.Write("ClientSocket.WorkerThread() OnFreeTime" & vbCrLf)
                                    End If
                                Else
                                    _isFreeTime = False
                                End If
                            Catch ex As ObjectDisposedException

                            Catch ex As NullReferenceException

                            Catch ex As Exception
                                'Throw ex
                            End Try
                        End If
                    Catch ex As ObjectDisposedException
                        Exit Do
                    Catch ex As Exception
                        'Throw ex
                    End Try
                End If

                Thread.Sleep(500)
            Loop

        End Sub

        '发送数据
        Public Function Send(ByVal bytes() As Byte) As Integer
            If bytes Is Nothing Then Return -1
            Send = 0

            SetNotFreeTime()
            _isBusy = True
            Try

                Send = _socket.Send(bytes, SocketFlags.None)
                'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
                If Send = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                    Return -1
                End If
            Catch ex As SocketException
                If ex.SocketErrorCode = SocketError.TimedOut Then
                    Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                    Return -1
                End If
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
            Catch ex As ObjectDisposedException
                Return -1
            Catch ex As NullReferenceException
                Return -1
            Catch ex As Exception
                Throw ex
            Finally
                SetNotFreeTime()
                _isBusy = False
            End Try

        End Function
        Public Function Send(ByVal bytes() As Byte, ByVal length As Integer) As Integer
            If bytes Is Nothing Then Return -1
            Send = 0

            SetNotFreeTime()
            _isBusy = True
            Try

                Send = _socket.Send(bytes, length, SocketFlags.None)
                'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
                If Send = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                    Return -1
                End If
            Catch ex As SocketException
                If ex.SocketErrorCode = SocketError.TimedOut Then
                    Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                    Return -1
                End If
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
            Catch ex As ObjectDisposedException
                Return -1
            Catch ex As NullReferenceException
                Return -1
            Catch ex As Exception
                Throw ex
            Finally
                SetNotFreeTime()
                _isBusy = False
            End Try

        End Function
        Public Function Send(ByVal bytes() As Byte, ByVal offset As Integer, ByVal size As Integer) As Integer
            If bytes Is Nothing Then Return -1
            Send = 0

            SetNotFreeTime()
            _isBusy = True
            Try

                Send = _socket.Send(bytes, offset, size, SocketFlags.None)
                'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
                If Send = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                    Return -1
                End If
            Catch ex As SocketException
                If ex.SocketErrorCode = SocketError.TimedOut Then
                    Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                    Return -1
                End If
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
            Catch ex As ObjectDisposedException
                Return -1
            Catch ex As NullReferenceException
                Return -1
            Catch ex As Exception
                Throw ex
            Finally
                SetNotFreeTime()
                _isBusy = False
            End Try

        End Function
        Public Function Send(ByVal str As String) As Integer

            Send = 0

            Dim bytes() As Byte
            bytes = _encoding.GetBytes(str)

            SetNotFreeTime()
            _isBusy = True
            Try

                Send = _socket.Send(bytes, bytes.Length, SocketFlags.None)
                'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
                If Send = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                    Return -1
                End If
            Catch ex As SocketException
                If ex.SocketErrorCode = SocketError.TimedOut Then
                    Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                    Return -1
                End If
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
            Catch ex As ObjectDisposedException
                Return -1
            Catch ex As NullReferenceException
                Return -1
            Catch ex As Exception
                Throw ex
            Finally
                SetNotFreeTime()
                _isBusy = False
            End Try

        End Function

        '接收数据
        Public Function Receive(ByVal length As Integer) As Byte()
            ReDim _buffer(length - 1)

            SetNotFreeTime()
            _isBusy = True
            Try
                If _socket.Receive(_buffer, 0, length, SocketFlags.None) = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.Receive() Receive Failed" & vbCrLf)
                End If

            Catch ex As SocketException
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.Receive() Receive SocketException" & vbCrLf)
            Catch ex As Exception
                Throw ex
            Finally
                SetNotFreeTime()
                _isBusy = False
            End Try

            Return _buffer
        End Function
        Public Function Receive(ByVal buffer() As Byte, ByVal offset As Integer, ByVal size As Integer) As Integer

            SetNotFreeTime()
            _isBusy = True
            Try
                Receive = _socket.Receive(buffer, offset, size, SocketFlags.None)
                'Debug.Write("ClientSocket.Receive() Receive : " & Receive & vbCrLf)
                If Receive = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.Receive() Receive Failed" & vbCrLf)
                End If

            Catch ex As SocketException
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.Receive() Receive SocketException" & vbCrLf)
            Catch ex As Exception
                Throw ex
            Finally
                SetNotFreeTime()
                _isBusy = False
            End Try

            Return Receive
        End Function

        '接收缓冲中所有存在的数据
        Public Function ReceiveExisting() As String
            Dim length As Integer = _socket.Available
            If length <= 0 Then Return String.Empty
            Dim _buffer(length - 1) As Byte
            ReceiveExisting = String.Empty

            SetNotFreeTime()
            _isBusy = True
            Try
                If _socket.Receive(_buffer, 0, _buffer.Length, SocketFlags.None) = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.Receive() Receive Failed" & vbCrLf)
                Else
                    ReceiveExisting = _encoding.GetString(_buffer)
                End If

            Catch ex As SocketException
                socketException(ex.SocketErrorCode)
                Debug.Write("ClientSocket.Receive() Receive SocketException" & vbCrLf)
            Catch ex As Exception
                Throw ex
            Finally
                SetNotFreeTime()
                _isBusy = False
            End Try

        End Function

        Private Sub SetNotFreeTime()
            _isFreeTime = False
            _lastTicks = DateTime.Now.Ticks
        End Sub


        Protected Sub BeginHeart()
            Dim length As Integer = 0
            Dim beginTicks As Long = Now.Ticks
            Dim nowTicks As Long = Now.Ticks
            ReDim _heartData(_heartDataServer.Length - 1)

            Try
                length = _socket.Send(_heartDataClient, SocketFlags.None)
                Debug.Write("ClientSocket.BeginHeart() Send HeartData " & length & vbCrLf)
                If length = 0 Then
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                    Debug.Write("ClientSocket.BeginHeart() Send HeartData OnDisconnect" & vbCrLf)
                    Return
                End If

                Do While nowTicks - beginTicks < _heartWaitTime * 10000
                    Try
                        length = _socket.Receive(_heartData, SocketFlags.None)
                        Debug.Write("ClientSocket.BeginHeart() Receive HeartData " & length & vbCrLf)
                        If length = 0 Then
                            If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                            Debug.Write("ClientSocket.BeginHeart() Receive HeartData OnDisconnect" & vbCrLf)
                            Return
                        Else
                            _isFreeTime = False
                            _lastTicks = DateTime.Now.Ticks

                            If CheckHeart(_heartData) Then
                                Debug.Write("ClientSocket.BeginHeart() Is HeartData" & vbCrLf)
                            Else
                                _buffer = _heartData
                                If _isDisconnected = False Then RaiseEvent OnReceive(Me, New ReceiveEventArgs(_buffer.Length))
                                Debug.Write("ClientSocket.BeginHeart() Not HeartData: " & Me._encoding.GetString(_buffer) & vbCrLf)
                            End If

                            Exit Do
                        End If
                    Catch ex As SocketException
                        socketException(ex.SocketErrorCode)
                    End Try

                    nowTicks = Now.Ticks
                    Thread.Sleep(100)
                Loop
                'length = _socket.Receive(_heartData, SocketFlags.None)
                'If length = 0 Then
                '    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                '    Return
                'End If

            Catch ex As SocketException
                socketException(ex.SocketErrorCode)
            Catch ex As Exception

            End Try
        End Sub

#Region "Heart Beat Functions"

        Protected Function CheckHeart(ByVal bytes() As Byte) As Boolean
            If bytes.Length = _heartDataServer.Length Then
                For i As Integer = 0 To bytes.Length - 1
                    If bytes(i) <> _heartDataServer(i) Then
                        Return False
                    End If
                Next i
                Return True
            End If
            Return False
        End Function

#End Region


        '关闭客户端
        Public Sub Close(ByVal timeout As Integer)
            If _isDisconnected Then Return
            Try
                _isDisconnected = True
                _isFreeTime = False

                _isReceiving = False
                If _thread IsNot Nothing Then
                    '_thread.Abort()
                    _thread = Nothing
                End If

                _socket.Shutdown(SocketShutdown.Both)
                _socket.Close(timeout)
                Debug.Write("ClientSocket.Close()" & vbCrLf)
            Catch ex As ObjectDisposedException

            Catch ex As Exception
                Throw ex
            End Try
        End Sub

        Public Sub ForceClose()
            Try
                _isDisconnected = True
                _isFreeTime = False

                _isReceiving = False
                If _thread IsNot Nothing Then
                    '_thread.Abort()
                    _thread = Nothing
                End If

                '_socket.Disconnect(False)
                '_socket.Shutdown(SocketShutdown.Both)
                _socket.Close(0)
                Debug.Write("ClientSocket.ForceClose()" & vbCrLf)
            Catch ex As ObjectDisposedException

            Catch ex As Exception
                Throw ex
            End Try
        End Sub

        Protected Sub socketException(ByVal errorCode As SocketError)
            Monitor.Enter(Me)
            Debug.Write("ClientSocket.socketException() : " & errorCode.ToString() & vbCrLf)
            Try
                Select Case errorCode
                    Case SocketError.ConnectionReset '远程主机强制关闭, 网线断开
                        If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.ConnectionReset))
                    Case SocketError.ConnectionAborted '远程主机Socket终止
                        If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.ConnectionAborted))
                    Case SocketError.Shutdown '远程主机关闭连接
                        If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Shutdown))
                    Case SocketError.TimedOut
                        If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.TimedOut))
                    Case SocketError.ConnectionRefused '远程主机未开始服务
                        _socket.Close()
                        'm_clientSocket.Close() : m_clientSocket = Nothing
                        'm_allDone.Close() : m_allDone = Nothing
                        'RaiseEvent OnConnectionRefused(Me, EventArgs.Empty)
                        ''Throw New AppException(errorCode.ToString())
                    Case SocketError.HostUnreachable '网卡禁用
                        'RaiseEvent OnHostUnreachable(Me, EventArgs.Empty)
                    Case SocketError.WouldBlock

                    Case SocketError.NotSocket

                    Case SocketError.AddressAlreadyInUse
                        'Me.Close(0)
                        'Throw New AppException("AddressAlreadyInUse")
                    Case SocketError.Interrupted

                    Case SocketError.NotConnected

                    Case Else
                        Throw New Exception(errorCode.ToString())
                End Select
            Finally
                Monitor.Exit(Me)
            End Try

        End Sub

    End Class

End Namespace
